avatar

目录
301 - 拷贝构造函数深拷贝

拷贝构造函数深拷贝

🧑‍💻 User

In this lecture we’re going to learn about copy constructors, and these are constructors that are calledby the compiler to make copies.If you try to construct a class object from another class object.To really learn about this, we’re going to use a person class.We’re going to set up a person class.It’s going to have two sections.We’re going to have a private section and we’re going to have a public section.The private section is going to have a few member variables.We’re going to have the last name, which is modeled as an stdstring.We’re going to have the first name and we’re going to have the age and the public section.We’re going to have a few constructors.We have the default constructor, which is not going to do anything.It’s just going to initialize the member variables.However, they were declared, so the name is going to be empty.The first name is going to be empty and the last name is going to be empty and the age is just goingto be a null pointer because the age is modeled as a pointer here.And this is by purpose because there are a few things I want you to see about copy constructors downhere.We have a few other constructors.We have one that only takes the last name, one that takes two parameters, the last name and the firstname, and another that takes all the other parameters, which is going to initialize all our membervariables here.We also have a utility function which is going to print information about a person object.It’s going to print the memory address where the object lives, and then we’re going to have the lastname, the first name the age, and we can even print the address here.We can print information about our person object here.This is the point.Okay.Now that we know about our class, let’s talk about copies.We have a person object here.We’re going to set them up and put this information here.The last name is going to be John.The first name is going to be snow and the age is going to be 25.And we can print information about this person, but we can create a copy of this person using the syntaxhere.And C plus plus is going to understand this so we can say person.Person two And we want to create a person two from person one here.And if we do this, this is going to work, but it’s not going to probably do what you expect becausethis is going to copy the member variables one by one from person one to person two.So here is a simple illustration.We have a person one, and we want to create a copy of person one in another person which is calledperson two or P two here.So what this is going to do is do a member wise copy.So it’s going to take the last name in person one and copy that in person.Two, we’re going to have a situation like this.It’s going to take the first name in person one and copy that in person.Two, we’re going to have a situation like this and then it’s going to copy the age from person onein person two But remember, the age is a pointer.It’s going to copy the address that is contained in this pointer and put that in person.Two here.And this is going to give us a few problems as we are about to see in the next slides.And copying pointers directly like this is really going to give us problems.Let’s explore this a little more.Here.We have a simple example where we have our person one and person two here and the person two is a copyof person one, as you see through the syntax here.So after we have our two persons created, suppose we want to set the age of person one.We do that through a set age setter that we have in person, which is going to set the age.And after that, if we print the information, we’re going to see that the age for person two was alsochanged because what we really copied is a pointer and the set age is going to dereference the pointerand change what is inside and the change in person.One is also going to be in person.Two So you need to be aware of this.This is a problem and C plus plus is going to introduce this problem by default because C plus plusis going to copy member variables one by one by default, and it’s going to really even copy pointers.So C plus plus does things this way because it generates a default copy constructor for you.And this is a constructor that is going to be used to do the copy we do here.So the syntax we use here is going to call a copy constructor.And in this case we don’t have a copy constructor in person.So the compiler is going to generate a copy constructor and use that.But that generated copy constructor is going to be doing member wise copy from person one to persontwo and it is going to blindly copy the pointer for the age member variable.This is the source of all the problems we have down here of also having the age for person two modifiedthrough changing the age of person one.Hopefully you see the problem here.Okay, so we have a problem here.How do we solve that?The first step is to.Try and see the problem and really prove this, for example, through the debugger.And right now we are using a constructor that is generated by the compiler.So we can’t really set a breakpoint there or really put any stdcout statement to see what is happening.But not to worry.It is possible to set our own copy constructor and use that to see information.We can even go further and disable the default behavior of even copying pointers and really customizeit however we want.So we’re going to do that.Here is our first attempt at setting up a copy constructor and we’re going to set it up like this.We’re going to set up a constructor which is called Person.This is part of the person class because of this scope resolution operator here.And then we’re going to pass a parameter which is another person by const because we don’t really wantto modify it.We want to read from it.But notice that we are passing this by value.You need to save this.And then in our initializer list, we’re basically going to be copying from the source person to ourperson object that we are building here.So the last name is going to be initialized with the last name from the source person.The first name is going to be initialized with the first name from the source person and the age isgoing to be initialized with the age from the source person.Here we are basically copying member wise.We are doing the same thing the compiler has done for us.So but this is going to be problematic because if you look here we are passing by value and this syntaxis not allowed to be used in C plus plus.The problem is this could lead to an endless chain of copy constructor calls.So why is this?Remember to create an object, we say something like this, we say person P two and use P one to buildour person two object.So when the compiler sees a call like this, it will know that it needs to call a copy constructor.And when it calls the copy constructor, it’s going to see our copy constructor here.But it’s going to see that something is passed by value.And to pass by value you need to call the copy constructor.But the copy constructor itself is taking something by value, so we’re going to keep trying to callthe copy constructor and this is going to be an endless chain of calls to the copy constructor and thisis going to end nowhere.So the designers of C plus plus saw this problem and they disallowed this syntax here to try and setup a copy constructor using an object that you pass by value here.So this is not going to work.If you do this, you’re going to get a compiler error.What if we try and pass by reference?Okay, so the first attempt here is really bad.It’s not going to work.If you’re trying to use it, you’re going to get a compiler error.But we can try and pass by reference and that’s what we have here.We are passing our person source person by reference here and it is by const reference because we don’treally want to modify it and we are doing the same thing we did in the last attempt in our initializerlist here.Last name is going to be initialized with the last name of the source person.First name is going to be initialized with the first name of first person here or source person I shouldsay.And the age is going to be initialized with the age of source person here.This is going to do what the C plus plus compiler does.By default, it is doing member wise copy and we are blindly copying the age member variable here andit is a pointer.So we’re going to take the pointer address contained in source and just copy that in destination here,which is this age member variable here.And this is going to also be bad because we just copy the pointer.And if we modify one person’s age, the change is going to be seen in the other person’s age.And we don’t really want that.Okay, so this is bad also, but it’s not as bad as the first attempt, but this is still bad.We need to do better.What we need to do really is to not blindly copy pointers, but set up a new memory address in our newperson object and just copy the value from the source object.And that’s what we are doing here.So we are copying the last name and the first name as we did in the last two attempts.But notice what we do for the age member variable inside.We’re going to set up a new memory location and we’re going to use the data from the source object toinitialize this new memory location.And this is going to do what we want.It is not going to do a blind copy of the pointer.It is going to set up a new memory location in our new person object, and then we’re going to use thedata in the source object.And this is good.And if we try to change the person one here, the changes are not.Going to leak into person two and this is going to be good.Okay, so this is what you should do.If you want to set up a copy constructor, you should pass your parameter by const reference as we dohere.And if you have any pointer member variables, you shouldn’t really blindly copy them.You should set up a new memory location in your destination object and then initialize that with thedata from the source object, which is what we are doing here.We are dereferencing the pointer from the source object and we are grabbing the data inside and usingthat to initialize our age pointer here.And this is really good.Okay, so this is one way we have to set up a copy constructor and it is going to work, but it is alsopossible to delegate the work on another constructor.And the last lecture we have learnt about constructor delegation.We can use that with copy constructors and we can do something like this.This is a copy constructor.It is going to take its parameter by const reference here, but notice that we are delegating the workto another constructor, which is going to take three parameters.So this constructor is going to use the data from our source object and we’re going to initialize thelast name here.We’re going to initialize the first name here and notice that we are dereferencing the pointer and passingthat data to our other constructor because that other constructor is going to set up a new memory locationand use this data here.Okay, so this is how you can delegate the construction from your copy constructor to another constructorthat is going to do the heavy lifting for you.I realize all this can really be confusing, so we are going to head over to Visual Studio code anddo this step by step so that you can really see, okay, here we are in our working folder.The current project is copy constructors.We’re going to grab our template files and use those.We’re going to paste those in and we are going to open this in Visual Studio code.Let’s do that by dragging and dropping here.This is going to open our folder and we need our person class that we can use to really learn aboutcopy constructors.We’re going to set up a header file which is going to be person dot H, and we’re going to set up anotherfile, which is going to be person dot CPP and we’re going to put this in place.I am going to put in the code I have prepared before so that we can really be fast with these things.This is really a simple class.It is called person.We have our member variables, we have a private section and a public section.The private section is going to contain our member variables and we’re going to have our constructorshere, which are going to be used to construct objects of person.We have a destructor, we have a few setters, we have a few getters, and we have a utility functionwhich is going to print information about our person object.Here.I am also going to put in the implementation for our person class and then I am going to try and explainit a little bit.So we have a few member variables.Last name and first name are modeled as STD strings and the age is modeled as a pointer because I wantyou to see problems that might come up if you set up copy constructors and you have pointer member variables.So we really need this.So the constructors are really going to do nothing special.We have a one parameter constructor which is going to delegate the work to another constructor, whichis going to take two parameters.The two parameter constructor is also going to delegate the work to a three parameter constructor,and the three parameter constructor is going to construct our object using this initializer list here.Notice that the three parameter constructor is going to set up a new memory location because if youremember the age is a pointer, so we need to allocate memory to storing the data.So that’s what we do in the constructor here.We’re going to say New Ant and we’re going to store in the data that is coming from the age parameterhere and we’re going to set up our object like this.The problem with this is that it is really using strings by copy and this is bad.So we should fix this either by using a const string references or using string views.I am going to use strings that we pass by reference here.So let’s fix this pretty quick.We’re going to say const string by reference, by reference we should say, and we’re going to say conststring by reference here and const string by reference.Let’s do this and let’s do that here as well.I think we have one left here.Let’s do that const string reference and we could use these parameters in our constructors here.So let’s go back in the CPP file and apply the same changes.We’re going to say const string reference here.You see we have a few squiggly lines, let’s say const string reference.We’re going to fix this class a little bit because string.Reference here.Let’s do this.You don’t have to do this.You can just grab the file that will be attached to the resource section of this lecture, and you canuse this class as is here.So let’s say const here and we’re going to say const string reference, let’s do that and const STDstring and we’re going to pass that by reference here and we have our class.So again, we have three member variables and one is a pointer and we have to make sure that the constructorsthat we use allocate new memory to be able to store data in this pointer here.This is what is really important for this class.So now that we have the class, we can head over in the main function and really use it.We’re going to bring up the sidebar here and head over in the main function and we’re going to includeour person class.So let’s include person.You see that we have this available in the main CPP file here.We’re going to take out what we don’t need and then we’re going to set up a person object.We’re going to say person P one and we’re going to put in John and the first name is going to be snowand the age is going to be 25.I think this is what we used in the slides.So let’s use this and we can print information about this person.Let’s do this and we’re going to build and see this printed out on the console.We can close this guy here.We don’t need this for now.You see, the world is good.We can bring up a terminal to really play with us and we’re going to run Rooster.We’re going to see that we have a person object living at this memory location.The last name is John.The first name is Snow.The age is 25, and the member variable for the age is living at this address here.This is the information we get from our print function, which is living in our header file.Let’s look at it and really see this.So it’s going to print this information here and it’s going to print the value we have in the age pointer.And it’s also going to print the memory location for this pointer here.And we do that by directly printing the pointer.Again, remember the age member variable here is a pointer.And this is key for what we are trying to do in this lecture here.So let’s try and create a copy.We’re going to go down here and create a new person.Person two We can say p two here, and the syntax to create a person object from another person is passingit as a parameter.So we’re going to pass P one here and this is going to do what we want.This is going to copy data from person one and use that to create a new person object, which is goingto be P two here.And if we try to print P two, we’re going to say print info.Let’s see what we get.The first thing you should notice here is that the syntax here is working because of some heavy liftingthat the compiler is going to do.For us to allow for this syntax here to work.Let’s build and really save this.We’re going to build with GCC, we’re going to see that the world is good.We can clear and run Rooster.Now we’re going to see that person.One last name is John.First name is age Address.Memory is here and you see that we have the same information in person to person two is going to liveat a different address because it is a separate variable.But what we have inside the last name is John.The first name is Snow.The age is 25.But you see that the address for the age member variable is the same.So it was copied over.So suppose we have three slots and these two person objects, we have one for the last name, we haveanother for the first name and we have another for the age.And we have the same things for person two here.So let’s put three slots here and what the compiler is going to do.If we set up a syntax like this, it’s going to do blind copy of the member variable.So it’s going to have what we have in the last name member variable and it’s going to copy that in theperson to object eight.It’s going to copy what we have in the last name and it’s going to paste that into the person to object.It’s going to copy what we have in the age member variable and it’s going to paste that into the agemember variable of person two But remember, the age is modeled as a pointer in our class.And what this is going to do, it’s going to copy the memory location, which is contained in personone, and it’s going to paste that into person two.And that’s what we see here.We see the memory address in person one For the age member variable.It’s the same as we have in person two So this is going to cause a few problems.So for example, if we try and modify person one, the changes are going to be seen even in person.Two for the age.So for example, let’s modify the age for person one, we’re going to set age and we’re going to setthe age to 30, for example, and then we’re going to print information for person one.And.Person two After that we’re going to say P one Printinfo and we’re going to say P two Printinfo.Here, if we do this, we’re going to see that the age for these two persons is the same.Even if we really change the age for person one.Let’s put a little separator here so that we can see this on the terminal.We’re going to put a few dashes.Think this is going to do and we’re going to build this.The bold is going to be good.And if we run this, let’s clear and run Rooster, we’re going to see that the age for person one andperson to have changed to 30 because if we call set age, it is going to dereference the pointer andchange the value inside.The thing is we have the same memory addresses in these two persons object and if you change eitherof these guys, the changes are going to be reflected in both of them.Let’s look at how we change the age in person.I don’t think we really looked at this in detail.Okay, so this is our set age method and you’re going to see that in the body.We’re going to be dereferencing the age pointer and assigning a value to it.And if we do this in main and change the age for one object and they happen to contain the same address,the changes are going to be reflected in both of them.And this is not what we want.What we would want if we copied a person object like this is for person two not to be affected by thechanges we do in person.One And the reason for this problem is because the compiler is doing blind memberwise copy.Again, it is copying the last name in person.Two It is copying the first name we have in person one in person two And it is copying what we havein the age member variable in person two And we have the same memory addresses here.So we need to fix this.But before I show you how to fix this, we should also show you how to see these things in the debugger.So, for example, let’s set up a breakpoint on line 14 here and we can use a debugger to see thesethings instead of printing them out using the print function here so we can run our debugger.We’re going to build our program.The build is good.We’re going to give control to the debugger.We’re going to hit this breakpoint here.And now we have our two person objects as locals in the main function here.So we can, for example, see the last name.Let’s expand that a plus here and let’s expand this a little bit.And you’re going to see that the data in here is John.For the last name.We can see the first name.We’re going to see that if we expand data.Plus here, we’re going to see that the last name is snow.We can go down and look at the age.You see that it is a pointer.Okay.So let’s go down to P two.We’re going to see that we have the same information in last name and first name.But look at the age pointer here.It contains the same address both for person one and person two.If you want to see the data inside, you can dereference the pointer and see the data inside.You can see that through the debugger.And this proves that we really are doing a member wise copy and we are just copying the pointer andthis is going to be problematic.If we modify one object, the changes are going to be seen in the other object.What we really need to do is to set up a new memory location in person two and use that to contain ourdata that we pass from person one.So we’re going to be able to do that if we set up our own copy constructor.So let’s head over in the person class and set up a copy constructor.One attempt one might want to do to set up a copy constructor is to do something like this.They might come and say Person, let’s say copy constructor.They might say person and then pass another person by value, let’s say person.If I can type that and we’re going to call this source P for source person and you see that we havea squiggly line, but let’s go on and really finish this up.And then what you might do is go in your implementation file because that’s where we have the implementationsfor our person class.You might say person and say person, we can pass what we had in the header file.And then what you might do is initialize your member variables from what you have from the source personhere.So we’re going to put in a body and then we’re going to put our initializer list on the next line.I think that’s going to be better.So we’re going to say M last name is it called M Last name.Let’s see and see.It is called Last Name, so let’s use that here.I don’t want to go back and change that.We’re going to say last name.It’s going to be initialized with what is coming from the source person.So we’re going to say source P and get last name.We have a function to give that and then we’re going to initialize the first name.It’s going to be using whatever comes from source.P So we’re going to get last name.We’re going to do that and then we’re going to put a comma and initialize the member variable, andwe’re going to use whatever comes from source person and we’re going to get age and use that to initializeour member variable.You see that this is really not working.And the reason is what we talked about in the slides.If you try to set up a copy constructor that passes parameter by value, this is going to cause a chainof endless copy constructor calls because the copy constructor is going to want to call a copy constructorto set up a copy and this is going to be bad.C plus plus designer saw that and they disallowed this kind of syntax here.So this syntax is not going to work.So let’s comment this out because some people are going to try and set up something like this.And let’s say that this is bad and say potential for endless copy constructor calls.Okay, So we said that here.Let’s close this a little bit because we need some more breathing space so we can bring this back.Okay.Now that we know that the syntax here is not going to work, we’re going to set up another one, whichis going to do member wise copy and we’re going to set it up like this.We’re going to pass by reference, so we’re going to say person and we’re going to pass our parameterby const reference.So we’re going to say const person reference and say source P here.This is going to be our source person.We’re going to head over in the CPP file and reuse what we had done before.We can copy this and we’re going to put in our copy constructor, which is going to pass by referenceand do member wise copy.So let’s say that here we’re going to do member wise copy and we need to call these guys as functionshere.This is what the squiggly lines here are about.I think this is the same problem we had with our bad constructor here.So let’s go back and comment this out because I want you to see the problem the compiler is going toshow you and we are going to uncomment this.Sorry for this and we are going to comment out the member wise copy constructor here and we’re goingto uncomment this, but we’re going to see that we get a squiggly line because this is not allowed syntaxin C plus plus.So let’s change this to do a call to the getter function.Okay, so our getters are called properly, but this is still not allowed.If we try to build our program, we’re going to get a compiler error that is going to say that thisis a syntax that is not allowed.The compiler error is not going to be really understandable.It’s going to say invalid constructor.You probably meant const person by reference.You see the error here.This is a syntax that is not allowed and if you’re trying to use it, you’re going to get a compilererror because it could lead to an endless chain of copy constructor calls.So we’re going to comment this out again because we have seen the compiler error.Let’s do that.Let’s head over in the header file and comment this out again and we are going to go down and uncommentour member wise copy constructor.We’re going to do this here and now we have a constructor that seems to work.If we try to build with this constructor in place, let’s see what we get.We’re going to build with GCC first.You see that we have errors and what do we mean by this compiler error?Okay, so what do we have here?Let’s go back and this is not really commented out.Let’s comment this out and we’re going to build again.Now you see that the build is good and we have a constructor that is going to simulate the behaviorwe had with the constructor that was generated for us by the compiler.Because remember, before we set up our own constructors, the compiler was generating a copy constructorfor us.And what the compiler generated was doing exactly what we see here.Member Wise copy.It was taking the last name from the source object and pasting that into our member variable for thelast name.It was taking the first name from our source object and pasting that into our first name and it wastaking the age and pasting that into our age member variable.And remember the age is a pointer and that was the source of our problems because we had two objectsfor the person class that were pointing to the same age memory location and this is really bad.And now that I look at this, I notice that we didn’t use the first name here.We used the last name, We can fix this.We’re going to say get first name.This is going to do what we want.But still we’re going to see the same problems we saw when we use the constructor that was generatedby the compiler.But we can prove that our constructor here is being called by the compiler.Let’s do that.We’re going to say Stdcout.I think we can say that and we’re going to say copy constructor called.We’re going to see that our copy constructor is being used by the compiler.If I can type endl here.So this is good.Let’s try and build and see what we get in our output.The world is going to be good.We can clear and run Rooster and we are going to see that our copy constructor is called and the personobject, which we created as P2 here, is going to have the same information we have in the first personobject.The last name is going to be John.The first name is going to be Snow.The age is going to be 25, but we blindly copied the memory address and the age pointer and pastedthat in our person object, which is P2 here.And this is going to lead to the problem we see here.If we change the age from person one, the change is even going to be seen in person.Two And this is not what we want.So we need to fix this problem.And to fix this we need to avoid blindly copying the pointer for the member variable for age.Here, we need to set up a new memory location and use that to store the information in our copy object.So we’re going to change the copy constructor we have here.We’re going to comment it out and do things right.So what we’re going to do is comment this out and we’re going to set up another one which is going tocopy properly.We’re going to say don’t blindly copy the pointer and we’re going to set up a copy constructor, whichis going to do that.We’re going to say person and we’re going to take a person object by const reference because this isthe syntax that C plus plus expects.What did we do?Const person by reference and we’re going to say Source P, This is going to be our person and we’regoing to go in the CPP file and comment out what we had here because we don’t want this, but I wantyou to keep this as a reference and you need to know that this is really bad.It’s not going to do what you want.And this is also bad because this is not supported.Let’s say that this is really bad.Okay?I think this is expressing the level to which this is really bad.So let’s go down and set up a constructor that is going to work, a copy constructor, I mean, andwe’re going to say don’t blindly copy pointers.So we’re going to grab the constructor we had, which had a problem and we’re going to modify it a littlebit and not blindly copy the pointer.So for the age remember variable, we don’t want to directly copy the pointer here.What we want to do is allocate new memory and we can do that in our initializer list here so we cansay new and, and this is going to dynamically allocate space on the heap.And what we store in here is what we get if we dereference the pointer we get from the source objectand we can use this syntax here to do that.So if we do this, we want to be blindly copying the pointer.We will be setting up a completely new memory location to store the data in our copy object.So now that we have this and we’re going to try and build and really show you this, we’re going tobuild with GCC first, we need to prove that this is going to work.So you see, the syntax is really working.The build is good.We can clear now and run Rooster Now we’re going to see that P one is going to live at this addressthat ends in D30P2 is going to live at this address that ends in C zero and the age member variablefor p one is going to live at an address that ends in 2D0 and the age for the second person object isgoing to live at a different address that ends in 320.So we are doing things right here.We are using a completely new memory location to store the age for a person two And this is what wewant.If we look at the output after we modify person one and change the age to 30, person one is going tohave the age of 30 and the person two is going to have the age of 25.And this is really what we want.We have a copy constructor which is doing what we want.It is not doing blind copies of member variables and this is what we want.This is how you can set up a copy constructor so you now know how to set up a copy constructor.But I want you to know that we can reuse the constructor we have on top here.If you look, this constructor already knows how to allocate new memory for the parameter that you giveit.So for the strings it’s going to do a copy.Last name is going to be the last name that we pass in the parameter here.But for the age, it is going to make a new memory location and it is going to store age parameter nso we can really reuse it and avoid setting up a copy constructor that is doing all the heavy job hereso we can comment it out and use delegated constructors.We can do that.So we’re going to say that here delegate from the copy constructor.And the way to do that we’re going to have the same syntax we had, but we’re not going to initializethe variables ourselves.So let’s copy this and.Adapt this because I don’t want to retype all this.So what we’re going to do is take out all the initialization we’re doing here and we’re going to delegatethe construction from the copy construction to the construction we have on top here.So we’re going to call that what we’re going to do is say person and we’re going to give it the parametersor the arguments we should say.So we’re going to give it the last name, we’re going to say source person, get last name.We can do that.And it is a function we should call it.We’re going to give it the first name, we’re going to say source person and we’re going to get thefirst name.Let’s do that.And it is a function we should call it.Then we’re going to give it the age.How do we get the age?We’re going to dereference the pointer that comes from get age and we’re going to say source person,get age now, not first name, it’s get age.We’re going to get the pointer and then we’re going to dereference that and pass the data to the otherconstructor that we have on top.So we still have a squiggly line here.I don’t know why.Maybe Cplusplus is confused.So what do we have here?Let’s check this a little bit.We need to see.We have source beget last name source beget first name and we are dereferencing the pointer here andpassing the data.And I think we need another set of parentheses.Let’s see if that’s what we need.Yes, we need another parentheses in here.And then the Visual Studio code is going to accept this.So try to take a moment and really understand what we are doing here.The copy constructor is set up using the syntax that Cplusplus accepts, but we’re not going to initializeour member variables ourselves like we did in the last copy constructor that we did.We’re not going to do any memory allocation ourselves.We’re going to delegate all that heavy work to the constructor that we already have, which is constructingour objects here.And this is going to work.It is going to do what we want.Let’s go back to the main function and build our program.We’re going to see that the world is good.We can clear and run Rooster.We’re going to see that we have the exact same output.We have the data copied from person one.We have the last name.We have the first name, we have the age.And if we change person one, as we do here in person one, we’re going to have the age to be 30.But person two is not going to be affected.And this is really good.This is what you should do, this is how you should set up your copy constructors.Okay, so this is really how you can set up your copy constructors in C plus plus.And I don’t think I really told you if you set up your own copy constructor C plus plus is no longergoing to set up a copy constructor for you.It is going to call your copy constructor and the copy constructors are another set of functions thatthe C plus plus compiler can generate for you if you don’t have them in and they are needed.We have seen that the C plus plus compiler can also generate constructors for you.It can also generate destructors if you don’t have any destructors and but it can also generate a copyconstructor as we have seen in this lecture.So you should really know that and we should make sure that in our destructor we are releasing the memorythat we allocated in our constructor.So let’s really check that out.We’re going to go in person.CP And look, we don’t have a destructor in place here.We have a destructor which is going to release the memory and this is really good.We have to make sure that we are not leaking any memory here.So this is really all we set out to do in this lecture.I hope you found it interesting.Again, remember the syntax to set up a copy constructor and remember to not do blind copies, especiallyif you have member variables that are pointers like our age.Here.We are going to stop here in this lecture and the next one we’re going to see that if you store elementsin an array, the compiler is actually going to make copies.And that’s really interesting and we’re going to see that in the next lecture.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,没问题!我们现在开始学习 C++ 中一个非常重要的概念:拷贝构造函数。

C++ 拷贝构造函数 (Copy Constructor) 深入解析:从浅拷贝到深拷贝

摘要: 本教程将带你深入理解 C++ 中的拷贝构造函数 (Copy Constructor),解释其工作原理、何时被调用,以及如何正确实现它以避免常见的浅拷贝问题,特别是处理指针成员时。我们将通过一个 Person 类的例子,一步步展示从默认拷贝构造函数的问题到自定义深拷贝构造函数的实现方法。


1. 详细解释

1.1 什么是拷贝构造函数?

拷贝构造函数是一种特殊的构造函数 (constructor)。它的作用是在使用一个已存在的、同类型的对象 (object)初始化 (initialize) 一个新的对象时被调用。简单来说,就是当你想要“复制”一个对象来创建一个全新的对象副本时,拷贝构造函数就会登场。

例如,如果你有一个 Person 类的对象 p1,然后你这样写:
Person p2 = p1;
或者
Person p2(p1);
编译器 (compiler) 就会尝试调用 Person 类的拷贝构造函数来创建 p2

1.2 我们的示例:Person

为了更好地理解拷贝构造函数,我们将使用一个 Person 类。这个类包含以下信息:

  • 私有成员 (private members):
    • lastName (姓): std::string 类型
    • firstName (名): std::string 类型
    • age (年龄): int* 类型 (一个指向整数的指针 (pointer))。这里特意使用指针是为了演示拷贝构造函数中可能出现的问题和解决方案。
  • 公有成员 (public members):
    • 多个构造函数,包括一个默认构造函数 (default constructor) 和带参数的构造函数。
    • 一个析构函数 (destructor),用于释放动态分配的内存。
    • 一个工具函数 print_info(),用于打印对象的信息。
    • 一些 settergetter 方法(用于设置和获取成员变量的值)。

关键点: age 成员是一个指针。在构造函数中,我们会为它动态分配内存来存储实际的年龄值。例如:
age = new int(actual_age_value);

1.3 默认拷贝构造函数和浅拷贝 (Shallow Copy) 问题

如果我们没有为 Person 类显式定义一个拷贝构造函数,C++ 编译器会自动为我们生成一个。这个默认的拷贝构造函数执行的是成员逐一拷贝 (member-wise copy)

  • 对于 std::string 类型的 lastNamefirstName,成员逐一拷贝会创建字符串内容的副本,这通常是我们想要的。
  • 但是,对于指针成员 age,成员逐一拷贝仅仅复制指针的地址值,而不是指针所指向的内存中的数据。这就导致了所谓的浅拷贝 (shallow copy)

图解浅拷贝:
假设我们有 Person p1;,它的 age 指针指向内存地址 0x1000,该地址存着年龄值 25
当我们执行 Person p2 = p1; 时,如果使用的是默认的(或错误的自定义)拷贝构造函数:

  • p2.lastName 会得到 p1.lastName 的副本。
  • p2.firstName 会得到 p1.firstName 的副本。
  • p2.age 会得到 p1.age 的副本,也就是说,p2.age 也将指向内存地址 0x1000

浅拷贝带来的问题:

  1. 数据修改联动: 由于 p1.agep2.age 指向同一块内存,如果通过 p1 修改了年龄(例如,p1.set_age(30),这会解引用 p1.age 指针并修改 0x1000 处的值),那么 p2 的年龄也会“莫名其妙”地变成 30,反之亦然。这通常不是我们期望的行为,我们希望副本是独立的。
  2. 悬挂指针和重复释放 (Dangling Pointer and Double Free):p1p2 的生命周期结束时,它们的析构函数都会尝试 delete age;。第一次 delete 会释放 0x1000 处的内存。当第二个对象的析构函数再次尝试 delete 同一个已经被释放的地址时,会导致程序崩溃(重复释放)。即使不崩溃,其中一个对象销毁后,另一个对象的 age 指针就成了悬挂指针,指向了无效内存。

1.4 自定义拷贝构造函数

为了解决浅拷贝的问题,我们需要自定义拷贝构造函数,并为指针成员实现深拷贝 (deep copy)。深拷贝意味着为新对象的指针成员分配一块新的内存,并将原对象指针所指向的数据复制到这块新内存中。

拷贝构造函数的典型声明格式如下:
ClassName(const ClassName& other_object);

  • ClassName 是你的类名。
  • 参数是 const ClassName& other_object
    • const: 表示我们不会在拷贝构造函数中修改源对象 other_object。这是一个好习惯。
    • &: 表示通过引用 (reference) 传递。这非常重要!

为什么必须通过引用传递?
如果我们尝试这样定义拷贝构造函数:Person(Person other_person); (按值传递)
当调用这个拷贝构造函数时,为了将实参传递给形参 other_person,需要先复制一份实参。但如何复制呢?又需要调用拷贝构造函数!这就形成了一个无限递归的调用链,最终导致栈溢出。因此,C++ 编译器禁止拷贝构造函数的参数按值传递,它必须是引用。

1.5 实现深拷贝的步骤

在我们的 Person 类的拷贝构造函数中:

  1. 对于非指针成员 (lastName, firstName),可以直接复制:
    lastName = other_person.lastName;
    firstName = other_person.firstName;
  2. 对于指针成员 age,需要执行深拷贝:
    • 首先,为新对象的 age 指针分配新的内存:age = new int;
    • 然后,将源对象 other_person.age 所指向的复制到这块新内存中:*age = *(other_person.age);
    • 注意: 在解引用 other_person.age 之前,最好检查它是否为 nullptr,以避免解引用空指针。在示例代码中,我们假设它总是在主构造函数中被正确初始化。

更简洁的写法(通常在构造函数的初始化列表中完成):
age = new int(*(other_person.m_age)); // 假设源对象的age指针名为m_age

1.6 拷贝构造函数和构造函数委托 (Constructor Delegation)

如果你的类中已经有一个构造函数(比如一个接收所有成员值的构造函数)正确地处理了所有成员的初始化(包括为指针成员分配新内存),那么你可以在拷贝构造函数中使用构造函数委托 (constructor delegation) 来避免重复代码。

例如,假设 Person 有一个构造函数:
Person(const std::string& ln, const std::string& fn, int val_age)
它内部会做 this->age = new int(val_age);

那么拷贝构造函数可以这样写:
Person(const Person& other) : Person(other.lastName, other.firstName, *(other.age)) {
// 委托给上面的构造函数,它会负责 new int(*(other.age))
std::cout << "拷贝构造函数 (委托版本) 被调用!" << std::endl;
}
这里,*(other.age) 解引用源对象的 age 指针,获取其指向的整数值,然后将这个值传递给目标构造函数,目标构造函数会为新对象的 age 指针 new 一块新内存并存入这个值。

1.7 何时调用拷贝构造函数?

拷贝构造函数主要在以下几种情况下被调用:

  1. 显式或隐式初始化:
    Person p2 = p1; // 隐式调用 (等号在这里是初始化,不是赋值)
    Person p2(p1); // 显式调用
  2. 函数按值传参: 当一个对象作为参数按值传递给一个函数时,会创建该对象的一个副本。
    void some_function(Person p);
    Person my_person;
    some_function(my_person); // Person的拷贝构造函数被调用以创建p
  3. 函数按值返回: 当一个函数按值返回一个对象时,可能会(取决于编译器优化,如RVO/NRVO)调用拷贝构造函数。
    Person create_person() {
    Person temp("Doe", "John", 30);
    return temp; // 可能调用拷贝构造函数
    }
    Person new_p = create_person();

1.8 拷贝构造函数 Vs 赋值运算符 (Assignment Operator)

需要区分拷贝构造函数和拷贝赋值运算符 (copy assignment operator)

  • 拷贝构造函数: 用于从一个已存在的对象 创建并初始化 一个 对象。
    Person p1("Snow", "John", 25);
    Person p2 = p1; // 调用拷贝构造函数
    Person p3(p1); // 调用拷贝构造函数

  • 拷贝赋值运算符: 用于将一个已存在的对象的值赋给 另一个已存在的 对象。
    Person p1("Snow", "John", 25);
    Person p2; // 调用默认构造函数 (或者其他构造函数)
    p2 = p1; // 调用拷贝赋值运算符 (operator=)

拷贝赋值运算符也需要处理深拷贝问题,并且通常会更复杂一些(比如需要检查自赋值,先释放旧资源再分配新资源)。这是另一个话题,但与拷贝构造函数密切相关。

1.9 析构函数的重要性

当类中动态分配了资源(如 new intage),必须提供一个析构函数 (destructor) 来释放在对象生命周期结束时分配的内存,以避免内存泄漏 (memory leak)
对于 Person 类,析构函数应该像这样:
~Person() {
delete age; // 释放动态分配的内存
age = nullptr; // 好习惯:将指针设为nullptr,防止悬挂指针
std::cout << "析构函数为 " << lastName << " 调用" << std::endl;
}

如果你提供了自定义的拷贝构造函数(尤其是涉及到动态内存管理时),几乎总是意味着你也需要提供自定义的析构函数和自定义的拷贝赋值运算符。这通常被称为 “Rule of Three” (三法则) (在C++11及以后,由于移动语义的引入,扩展为 “Rule of Five” 或 “Rule of Zero”)。


2. 代码示例

下面是一个完整的 Person 类示例,包括正确的拷贝构造函数(使用委托)和析构函数。

person.h (头文件)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef PERSON_H
#define PERSON_H

#include <string>
#include <iostream>

class Person {
private:
std::string lastName;
std::string firstName;
int* age; // 指针成员

public:
// 带所有参数的构造函数 (主要构造逻辑)
Person(const std::string& ln, const std::string& fn, int val_age);

// 默认构造函数 (委托给主要构造函数)
Person();

// 拷贝构造函数 (委托给主要构造函数,实现深拷贝)
Person(const Person& other);

// 析构函数
~Person();

// Setter for age
void set_age(int new_age);

// Getter for age value
int get_age_value() const;

// Utility function to print info
void print_info() const;
};

#endif // PERSON_H

person.cpp (实现文件)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "person.h"

// 带所有参数的构造函数 (主要构造逻辑)
Person::Person(const std::string& ln, const std::string& fn, int val_age)
: lastName(ln), firstName(fn) {
age = new int(val_age); // 为 age 分配新内存并初始化
std::cout << "构造函数 (带参) 为 " << lastName << " 调用, 年龄指针: " << age << std::endl;
}

// 默认构造函数 (委托给主要构造函数)
Person::Person() : Person("N/A", "N/A", 0) {
std::cout << "默认构造函数为 " << lastName << " 调用" << std::endl;
}

// 拷贝构造函数 (委托给主要构造函数,实现深拷贝)
Person::Person(const Person& other)
: Person(other.lastName, other.firstName, *(other.age)) { // 委托
// *(other.age) 获取源对象年龄值
// 主要构造函数会负责 new int(...)
std::cout << "拷贝构造函数为 " << lastName << " (从 " << other.lastName << ") 复制, 新年龄指针: " << age << ", 源年龄指针: " << other.age << std::endl;
}

// 析构函数
Person::~Person() {
std::cout << "析构函数为 " << lastName << " 调用, 准备释放年龄指针: " << age << std::endl;
delete age; // 释放动态分配的内存
age = nullptr; // 好习惯
}

// Setter for age
void Person::set_age(int new_age) {
if (age) {
*age = new_age;
}
}

// Getter for age value
int Person::get_age_value() const {
return (age ? *age : -1); // 如果age有效则返回值,否则返回-1
}

// Utility function to print info
void Person::print_info() const {
std::cout << "Person Info: [Address: " << this << "]" << std::endl;
std::cout << " Last Name: " << lastName << std::endl;
std::cout << " First Name: " << firstName << std::endl;
if (age) {
std::cout << " Age: " << *age << " (value stored at address: " << age << ")" << std::endl;
} else {
std::cout << " Age: N/A (pointer is null)" << std::endl;
}
std::cout << "-------------------------" << std::endl;
}

main.cpp (主程序,用于测试)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "person.h"
#include <iostream>

void demonstrate_shallow_copy_problem() {
std::cout << "\n--- 演示浅拷贝问题 (如果拷贝构造函数实现错误) ---" << std::endl;
// 假设有一个错误的拷贝构造函数实现 (仅用于演示概念)
// class BadPerson { public: std::string name; int* age_ptr;
// BadPerson(std::string n, int a) : name(n), age_ptr(new int(a)) {}
// BadPerson(const BadPerson& other) : name(other.name), age_ptr(other.age_ptr) {} // 浅拷贝
// ~BadPerson() { delete age_ptr; } };
// BadPerson bp1("Bad", 20);
// BadPerson bp2 = bp1; // bp1.age_ptr 和 bp2.age_ptr 指向相同地址
// *(bp1.age_ptr) = 22; // bp2的年龄也会变成22
// std::cout << "bp2 age: " << *(bp2.age_ptr) << std::endl;
// 此处bp1和bp2析构时会double free,导致崩溃
std::cout << "这个场景用文字描述:如果 Person p1 和 Person p2 的 age 指针指向同一内存," << std::endl;
std::cout << "修改 p1 的年龄会影响 p2,且析构时会重复释放内存导致程序崩溃。" << std::endl;
}

int main() {
std::cout << "--- 创建 p1 ---" << std::endl;
Person p1("Snow", "John", 25);
p1.print_info();

std::cout << "\n--- 使用 p1 创建 p2 (调用拷贝构造函数) ---" << std::endl;
Person p2 = p1; // 调用拷贝构造函数
p2.print_info();

std::cout << "\n--- 修改 p1 的年龄 ---" << std::endl;
p1.set_age(30);
std::cout << "p1 修改后:" << std::endl;
p1.print_info();
std::cout << "p2 (应该不受影响):" << std::endl;
p2.print_info(); // p2的年龄应该还是25,因为是深拷贝

std::cout << "\n--- 使用 p1 创建 p3 (另一种调用拷贝构造函数的方式) ---" << std::endl;
Person p3(p1);
p3.print_info();

demonstrate_shallow_copy_problem();

std::cout << "\n--- main函数结束,对象将按p3, p2, p1的逆序析构 ---" << std::endl;
return 0;
}

编译和运行:
你可以将 person.h, person.cpp, main.cpp 保存到同一个目录下,然后使用 C++ 编译器(如 g++)编译:
g++ main.cpp person.cpp -o copy_constructor_test -std=c++11
然后运行:
./copy_constructor_test

你会从输出中看到构造函数、拷贝构造函数和析构函数被调用的时机,以及 p1p2age 指针指向了不同的内存地址,修改 p1 的年龄不会影响 p2


3. QA 闪卡

  1. Q: 什么是拷贝构造函数 (Copy Constructor)?它在什么时候被调用?
    A: 拷贝构造函数是一种特殊的构造函数,用于通过一个已存在的同类对象来初始化一个新对象。调用时机:1. ClassName obj2 = obj1;ClassName obj2(obj1); 初始化。2. 函数按值传递对象参数。3. 函数按值返回对象。

  2. Q: 什么是浅拷贝 (Shallow Copy)?它对指针成员有什么问题?
    A: 浅拷贝是指在复制对象时,只复制成员变量的值。对于指针成员,只复制指针的地址,而不是指针指向的数据。问题:多个对象可能共享同一块内存,修改一个会影响另一个;对象析构时可能导致重复释放内存或悬挂指针。

  3. Q: 什么是深拷贝 (Deep Copy)?如何为指针成员实现深拷贝?
    A: 深拷贝是指在复制对象时,如果遇到指针成员,会为新对象的指针成员分配新的内存,并将原对象指针指向的数据复制到新分配的内存中。实现:在新对象的拷贝构造函数中,为指针成员 new 一块内存,然后解引用原对象的指针,将数据拷贝过来。

  4. Q: 为什么拷贝构造函数的参数通常声明为 const ClassName&
    A: const 是为了确保不修改源对象。& (引用) 是为了避免无限递归调用拷贝构造函数(如果按值传递,传递过程本身就需要拷贝)。

  5. Q: 如果类中有动态分配的内存(如使用了 new),拷贝构造函数和析构函数 (Destructor) 需要注意什么?
    A: 拷贝构造函数需要实现深拷贝,为新对象分配独立的内存资源。析构函数需要在对象销毁时使用 delete (或 delete[]) 释放动态分配的内存,以防止内存泄漏。


4. 常见误解或易犯错误

  1. 忘记自定义拷贝构造函数: 当类中有指针成员,并且你依赖编译器生成的默认拷贝构造函数时,会导致浅拷贝问题。
  2. 自定义了拷贝构造函数但仍是浅拷贝: 比如写了 age = other.age;(对于指针 age),这仍然是浅拷贝。正确的是 age = new int(*(other.age));
  3. 拷贝构造函数参数按值传递: Person(Person other); 这是不允许的,会导致编译错误,因为它会引发无限递归。必须是引用 Person(const Person& other);
  4. 忘记析构函数: 如果在构造函数或拷贝构造函数中用 new 分配了内存,但没有在析构函数中用 delete 释放,会导致内存泄漏。
  5. 混淆拷贝初始化和赋值:
    • Person p2 = p1; // 初始化,调用拷贝构造函数。
    • Person p2; p2 = p1; // p2 = p1; 是赋值,调用拷贝赋值运算符 (operator=),不是拷贝构造函数。
  6. 拷贝构造函数中忘记拷贝所有成员: 确保拷贝构造函数处理了类的所有成员变量,尤其是后来新添加的成员。
  7. 在深拷贝指针前未检查源指针是否为 nullptr: 如果源对象的指针可能是 nullptr,直接解引用 *(other.age) 会导致程序崩溃。应先检查:if (other.age) { age = new int(*(other.age)); } else { age = nullptr; }。在我们的例子中,主构造函数保证了 age 被初始化,所以简化了处理。

5. 编码练习

现在,轮到你来实践了!下面是一个 Book 类,它有一个动态分配的字符数组成员 title。请你为它补全拷贝构造函数的实现,确保进行深拷贝。

book.h

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef BOOK_H
#define BOOK_H

#include <iostream>
#include <cstring> // For strcpy, strlen

class Book {
private:
char* title; // 动态分配的书名
int pages;

public:
// 构造函数
Book(const char* t, int p);

// 拷贝构造函数 (需要你来实现)
Book(const Book& other);

// 析构函数
~Book();

void display() const;

void set_title(const char* new_title); // 一个修改title的方法,用于测试深拷贝
};

#endif // BOOK_H

book.cpp (你需要填充拷贝构造函数部分)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "book.h"

// 构造函数
Book::Book(const char* t, int p) : pages(p) {
if (t) {
title = new char[std::strlen(t) + 1]; // +1 for null terminator
std::strcpy(title, t);
} else {
title = new char[1];
title[0] = '\0'; // Empty string
}
std::cout << "Book '" << (title ? title : "N/A") << "' constructed." << std::endl;
}

// 拷贝构造函数 (TODO: 请你来实现这里的深拷贝逻辑)
Book::Book(const Book& other) : pages(other.pages) {
std::cout << "Book copy constructor invoked for creating a copy of '" << (other.title ? other.title : "N/A") << "'." << std::endl;
// --- 开始你的代码 ---
if (other.title) {
// 1. 为 this->title 分配足够的内存 (包括末尾的 '\0')
// title = new char[...];
// 2. 使用 std::strcpy 将 other.title 的内容复制到 this->title
// std::strcpy(title, other.title);
} else {
// title = new char[1];
// title[0] = '\0';
}
// --- 结束你的代码 ---
std::cout << "Book '" << (title ? title : "N/A") << "' (copy) constructed." << std::endl;
}


// 析构函数
Book::~Book() {
std::cout << "Book '" << (title ? title : "N/A") << "' destructed." << std::endl;
delete[] title; // 使用 delete[] 因为 title 是用 new char[] 分配的
}

void Book::display() const {
std::cout << "Title: " << (title ? title : "N/A") << ", Pages: " << pages << std::endl;
}

void Book::set_title(const char* new_title) {
delete[] title; // 删除旧标题
if (new_title) {
title = new char[std::strlen(new_title) + 1];
std::strcpy(title, new_title);
} else {
title = new char[1];
title[0] = '\0';
}
std::cout << "Book title changed to '" << (title ? title : "N/A") << "'." << std::endl;
}

main_exercise.cpp (用于测试你的实现)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "book.h"

int main() {
std::cout << "Creating originalBook..." << std::endl;
Book originalBook("The Lord of the Rings", 1200);
originalBook.display();
std::cout << std::endl;

std::cout << "Creating copiedBook from originalBook..." << std::endl;
Book copiedBook = originalBook; // 调用拷贝构造函数
copiedBook.display();
std::cout << std::endl;

std::cout << "Changing title of originalBook..." << std::endl;
originalBook.set_title("The Hobbit");
originalBook.display();
std::cout << "CopiedBook's title (should remain unchanged if deep copy worked):" << std::endl;
copiedBook.display(); // 如果是深拷贝,copiedBook的标题不应改变
std::cout << std::endl;

std::cout << "Creating anotherCopiedBook from originalBook..." << std::endl;
Book anotherCopiedBook(originalBook); // 再次调用拷贝构造函数
anotherCopiedBook.display();
std::cout << std::endl;

std::cout << "Program ending. Destructors will be called." << std::endl;
return 0;
}

请你将 book.cpp 中拷贝构造函数 Book::Book(const Book& other)// --- 开始你的代码 ---// --- 结束你的代码 --- 之间的部分补充完整。完成后,将 book.h, book.cpp, main_exercise.cpp 放在一起编译运行,看看结果是否符合预期(修改 originalBook 的标题不应影响 copiedBook)。

当你完成后,请将你修改后的 Book::Book(const Book& other) 函数的完整代码发给我,我会帮你检查。


🧑‍💻 User

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "book.h"

// 构造函数
Book::Book(const char* t, int p) : pages(p) {
if (t) {
title = new char[std::strlen(t) + 1]; // +1 for null terminator
std::strcpy(title, t);
} else {
title = new char[1];
title[0] = '\0'; // Empty string
}
std::cout << "Book '" << (title ? title : "N/A") << "' constructed." << std::endl;
}

// 拷贝构造函数 (TODO: 请你来实现这里的深拷贝逻辑)
Book::Book(const Book& other) : pages(other.pages) {
std::cout << "Book copy constructor invoked for creating a copy of '" << (other.title ? other.title : "N/A") << "'." << std::endl;
// --- 开始你的代码 ---
if (other.title) {
// 1. 为 this->title 分配足够的内存 (包括末尾的 '\0')
// title = new char[...];
// 2. 使用 std::strcpy 将 other.title 的内容复制到 this->title
// std::strcpy(title, other.title);
title = new char[std::strlen(other.title) + 1];
std::strcpy(title, other.title);

} else {
title = new char[1];
title[0] = '\0';
}
// --- 结束你的代码 ---
std::cout << "Book '" << (title ? title : "N/A") << "' (copy) constructed." << std::endl;
}


// 析构函数
Book::~Book() {
std::cout << "Book '" << (title ? title : "N/A") << "' destructed." << std::endl;
delete[] title; // 使用 delete[] 因为 title 是用 new char[] 分配的
}

void Book::display() const {
std::cout << "Title: " << (title ? title : "N/A") << ", Pages: " << pages << std::endl;
}

void Book::set_title(const char* new_title) {
delete[] title; // 删除旧标题
if (new_title) {
title = new char[std::strlen(new_title) + 1];
std::strcpy(title, new_title);
} else {
title = new char[1];
title[0] = '\0';
}
std::cout << "Book title changed to '" << (title ? title : "N/A") << "'." << std::endl;
}

🤖 Assistant

非常好!你对 Book 类拷贝构造函数的实现非常出色!


7. 评价你的解答与正确答案解析

评价你的解答

评分:5/5 (优秀)

你的代码成功地实现了 Book 类拷贝构造函数的深拷贝逻辑。

做得好的地方:

  1. 正确深拷贝: 你为新对象的 title 指针分配了新的内存空间,并使用 std::strcpy 复制了源对象 title 的内容。这是深拷贝的核心。
  2. 处理 nullptr 你正确地检查了 other.title 是否为 nullptr。如果源 titlenullptr(或指向空字符串,虽然题目中的构造函数会确保它至少指向 \0),你的代码会为新对象的 title 分配内存并将其初始化为一个空字符串 (\0)。这是一个安全且合理的处理方式。
  3. 成员初始化列表: 你使用了成员初始化列表 pages(other.pages) 来复制 pages 成员,这是推荐的做法。

你的实现是完全正确的,并且能够通过 main_exercise.cpp 中的测试,即修改一个对象的 title 不会影响其副本的 title

参考答案与进一步解释

你的实现本身就是一种正确的答案。下面我们再回顾一下关键点,并展示一个使用构造函数委托 (constructor delegation) 的替代版本,它可以帮助减少代码重复(如果适用的话)。

你的正确实现(再次强调):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 拷贝构造函数
Book::Book(const Book& other) : pages(other.pages) {
std::cout << "Book copy constructor invoked for creating a copy of '" << (other.title ? other.title : "N/A") << "'." << std::endl;
if (other.title) {
// 1. 为 this->title 分配足够的内存 (包括末尾的 '\0')
title = new char[std::strlen(other.title) + 1];
// 2. 使用 std::strcpy 将 other.title 的内容复制到 this->title
std::strcpy(title, other.title);
} else {
// 如果 other.title 是 nullptr (尽管我们的构造函数会避免这种情况,但防御性编程是好的)
// 或者如果 other.title 就是一个空字符串指针
title = new char[1];
title[0] = '\0';
}
std::cout << "Book '" << (title ? title : "N/A") << "' (copy) constructed." << std::endl;
}

解释关键步骤:

  1. pages(other.pages):对于像 int 这样的简单数据类型,直接在成员初始化列表中复制其值即可。
  2. if (other.title):检查源对象的 title 是否有效。这是一个好习惯,尽管在当前 Book 类的构造函数中,title 总是被初始化(要么指向一个有效的字符串,要么指向一个只包含 \0 的空字符串)。
  3. title = new char[std::strlen(other.title) + 1];:这是深拷贝的核心。为新对象的 title 分配一块全新的内存。内存的大小是源字符串的长度加上1(为了存储末尾的空终止符 \0)。
  4. std::strcpy(title, other.title);:将源字符串的内容复制到新分配的内存中。
  5. else 分支:如果 other.titlenullptr (或逻辑上为空),为新 title 创建一个表示空字符串的有效指针。

使用构造函数委托的替代版本:
如果你的主构造函数已经妥善处理了 title 的分配和复制逻辑(就像我们的 Book(const char* t, int p) 一样),你可以使用构造函数委托来简化拷贝构造函数,避免代码重复:

cpp
1
2
3
4
5
6
7
8
9
10
11
// 拷贝构造函数 (使用构造函数委托的版本)
Book::Book(const Book& other)
: Book(other.title, other.pages) { // 将工作委托给 Book(const char* t, int p)
// Book(const char* t, int p) 构造函数会处理 title 的 new 和 strcpy,
// 以及 pages 的初始化。
// 它也会处理 other.title 为 nullptr 或空字符串的情况。
std::cout << "Book copy constructor (delegating version) invoked for creating a copy of '" << (other.title ? other.title : "N/A") << "'." << std::endl;
// 注意:这里我们不需要再显式地操作 this->title 或 this->pages,
// 因为委托的构造函数已经完成了这些工作。
// 这条额外的 cout 只是为了演示。
}

这个版本更简洁,因为它重用了已有的构造逻辑。主构造函数 Book(const char* t, int p) 的实现如下:

cpp
1
2
3
4
5
6
7
8
9
10
Book::Book(const char* t, int p) : pages(p) {
if (t) {
title = new char[std::strlen(t) + 1];
std::strcpy(title, t);
} else {
title = new char[1];
title[0] = '\0';
}
std::cout << "Book '" << (title ? title : "N/A") << "' constructed (by main constructor)." << std::endl;
}

当拷贝构造函数 Book(other.title, other.pages) 被调用时,它实际上是调用了 Book::Book(const char* t, int p),并将 other.title 作为 t 传递,other.pages 作为 p 传递。主构造函数内的逻辑会为新对象正确地分配和复制 title

编译并运行你的代码的结果应如下所示(部分输出):

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Creating originalBook...
Book 'The Lord of the Rings' constructed.
Title: The Lord of the Rings, Pages: 1200

Creating copiedBook from originalBook...
Book copy constructor invoked for creating a copy of 'The Lord of the Rings'.
Book 'The Lord of the Rings' (copy) constructed.
Title: The Lord of the Rings, Pages: 1200

Changing title of originalBook...
Book title changed to 'The Hobbit'.
Title: The Hobbit, Pages: 1200
CopiedBook's title (should remain unchanged if deep copy worked):
Title: The Lord of the Rings, Pages: 1200 // <- 关键:copiedBook的标题未改变

... (析构函数调用) ...
Book 'The Hobbit' destructed.
Book 'The Lord of the Rings' destructed.
Book 'The Lord of the Rings' destructed.

你的实现应该能得到类似这样的正确结果。


8. 建议与扩展问题

  1. 拷贝赋值运算符 (operator=)

    • 你已经正确实现了拷贝构造函数和析构函数。根据 “Rule of Three” (三法则)(在现代C++中更倾向于 “Rule of Five” 或 “Rule of Zero”),当你手动管理资源并定义了这两者之一(或拷贝构造函数)时,通常也需要定义拷贝赋值运算符 (operator=)
    • 思考: Book 类如何实现拷贝赋值运算符?它需要处理哪些额外的情况(例如,自我赋值 bookA = bookA;,以及释放目标对象已有的 title 内存)?
      cpp
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      // Book.h 中声明
      // Book& operator=(const Book& other);

      // Book.cpp 中可能的实现框架
      /*
      Book& Book::operator=(const Book& other) {
      if (this == &other) { // 1. 检查自赋值
      return *this;
      }

      // 2. 释放当前对象的资源
      delete[] title;
      title = nullptr; // 置为nullptr,即使下一步new失败,状态也是安全的

      // 3. 复制源对象的简单成员
      pages = other.pages;

      // 4. 深拷贝动态分配的资源
      if (other.title) {
      title = new char[std::strlen(other.title) + 1];
      std::strcpy(title, other.title);
      } else {
      title = new char[1];
      title[0] = '\0';
      }
      std::cout << "Book assignment operator invoked." << std::endl;
      return *this; // 返回对当前对象的引用
      }
      */
  2. 使用 std::string

    • 如果 Book 类的 title 成员使用 std::string 而不是 char*,拷贝构造函数、析构函数和拷贝赋值运算符会变得多么简单?
    • 思考: 如果 titlestd::string title;,那么编译器自动生成的拷贝构造函数、析构函数和拷贝赋值运算符是否就能正常工作,不再需要我们手动实现深拷贝?(提示:是的,因为 std::string 内部已经正确处理了它自己的内存管理和深拷贝逻辑。)
  3. 异常安全 (Exception Safety)

    • 在你的拷贝构造函数中,new char[...] 可能会抛出 std::bad_alloc 异常(如果内存不足)。你的当前实现是异常安全的:如果 new 抛出异常,对象的构造过程会中断,部分构造的对象会被正确销毁(其成员的析构函数会被调用,但对于原始指针 title,在 new 失败时尚未赋值,所以没有内存泄漏)。
    • 对于拷贝赋值运算符,在 delete[] title; 之后,如果接下来的 new char[...] 失败,对象会处于一个有效的状态(titlenullptrpages 可能已更新)。这通常被认为是提供基本异常保证。
  4. 移动语义 (Move Semantics) (C++11及以后)

    • 除了拷贝构造函数和拷贝赋值运算符,现代C++还引入了移动构造函数 (move constructor)移动赋值运算符 (move assignment operator)。它们用于从临时对象或明确标记为可移动的对象“窃取”资源,而不是复制,从而提高效率。
    • 思考: Book 类是否能从移动语义中受益?移动构造函数会如何实现?

这些扩展问题可以帮助你更深入地理解C++中的对象生命周期管理和资源管理。做得非常好,继续努力!


🧑‍💻 User

有个问题, 如果一个class中没有指针, 那么是不是自动就能深拷贝


🤖 Assistant

你说得非常对,这是一个很棒的洞察!

是的,如果一个类中不包含任何原始指针 (raw pointer) 成员(即你自己用 new 分配并需要用 delete 管理的指针),那么编译器自动生成的拷贝构造函数通常就能实现我们期望的“深拷贝”效果,或者更准确地说,它会创建出一个完全独立的对象副本。

让我们来详细解释一下为什么:

  1. 成员逐一拷贝 (Member-wise Copy) 的行为:
    编译器生成的默认拷贝构造函数会对类的每个成员执行拷贝。

    • 对于基本数据类型成员 (如 int, double, char, bool 等):直接复制它们的值。这本身就是一种“深拷贝”,因为值被复制了,新对象有自己的独立值。
    • 对于对象类型成员 (如 std::string, std::vector, std::map,或者你自己写的其他没有原始指针的类对象):编译器会调用这些成员对象它们自己的拷贝构造函数
  2. 标准库容器和类的行为:
    std::string, std::vector, std::list, std::map 等标准库类,它们内部可能使用了指针来管理动态分配的内存(例如 std::string 内部有指向字符数组的指针,std::vector 内部有指向其元素存储区的指针)。
    但是,这些标准库类被设计得非常好,它们自己的拷贝构造函数已经正确地实现了深拷贝逻辑。
    所以,当你拷贝一个包含 std::string 成员的类时:

    • 你的类的默认拷贝构造函数被调用。
    • 对于 std::string 成员,它会调用 std::string 的拷贝构造函数。
    • std::string 的拷贝构造函数会分配新的内存,并把源字符串的内容复制到新内存中。
      结果就是,你的新对象的 std::string 成员拥有了自己独立的字符串数据副本。对于 std::vector 等其他容器也是同理。
  3. 结论:
    因此,如果你的类 MyClass 构成为:

    • 基本数据类型。
    • 其他类的对象,而这些类本身正确地实现了拷贝语义(比如标准库类,或者你自己写的、遵循了“三/五/零法则”的类)。
      那么,MyClass 的默认拷贝构造函数通过逐个调用其成员的(正确的)拷贝操作,最终会产生一个与源对象完全独立的新对象。修改源对象不会影响副本,反之亦然。在这种情况下,你通常不需要自己编写拷贝构造函数、析构函数或拷贝赋值运算符(这就是所谓的 “Rule of Zero” (零法则) 的精神:如果类不直接管理资源,就让编译器生成默认的特殊成员函数)。

例子:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <string>
#include <vector>
#include <iostream>

class Student {
public:
int id;
std::string name; // std::string handles its own deep copy
std::vector<int> scores; // std::vector handles its own deep copy

Student(int i, const std::string& n) : id(i), name(n) {}

void add_score(int score) {
scores.push_back(score);
}

void print() const {
std::cout << "ID: " << id << ", Name: " << name << ", Scores: ";
for (int score : scores) {
std::cout << score << " ";
}
std::cout << std::endl;
}

// 不需要自定义拷贝构造函数、析构函数、拷贝赋值运算符
// 编译器生成的版本就足够了
};

int main() {
Student s1(101, "Alice");
s1.add_score(90);
s1.add_score(85);

std::cout << "s1: ";
s1.print();

Student s2 = s1; // 调用编译器生成的拷贝构造函数

std::cout << "s2 (copy of s1): ";
s2.print();

// 修改 s2
s2.id = 102;
s2.name = "Bob";
s2.add_score(77);

std::cout << "\nAfter modifying s2:" << std::endl;
std::cout << "s1: ";
s1.print(); // s1 应该不受影响
std::cout << "s2: ";
s2.print();

return 0;
}

在这个例子中,Student 类没有显式定义拷贝构造函数。当你执行 Student s2 = s1; 时:

  • s2.id 会得到 s1.id 的值的副本。
  • s2.name 会通过调用 std::string 的拷贝构造函数来初始化,得到 s1.name 的深拷贝。
  • s2.scores 会通过调用 std::vector<int> 的拷贝构造函数来初始化,得到 s1.scores 的深拷贝。

因此,s1s2 是完全独立的对象。

总结一下:
“深拷贝”这个概念之所以如此重要和被强调,主要是因为当类中包含原始指针,并且该类直接负责这些指针所指向内存的分配和释放时,默认的成员逐一拷贝(即浅拷贝这些指针)就会出问题。如果类不直接管理这种原始指针资源,而是依赖于像 std::stringstd::vector 这样的“资源管理类”成员,那么通常就不需要担心深拷贝的问题,因为这些成员类会自己处理好。

所以,你的理解是正确的:没有(手动管理的)指针,通常默认就能得到功能上等同于深拷贝的独立副本。


评论